/*
 *    Copyright 2010-2013 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.google.code.jbauble.db.mysql;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.jbauble.db.Datastore;
import com.google.code.jbauble.db.Key;
import com.google.code.jbauble.db.Query;
import com.google.code.jbauble.db.exception.DataAccessException;
import com.google.code.jbauble.db.mapping.MappedClass;

public class MysqlQuery<T> implements Query {
	private final Logger log = LoggerFactory.getLogger(this.getClass());
	private boolean validateName = true;
	private boolean validateType = true;
	
	private String[] fields = null;
	private Boolean includeFields = null;
	private MysqlDatastore ds = null;
	private int offset = 0;
	private int limit = -1;
	private Class<T> clazz = null;
	
	private MysqlNative jdbc = null;
	private String fieldString = "";
	private String whereString = "";
	private String orderString = "";
	private List whereList = null;
	
	private boolean returnNull = false;
	
	private MappedClass mappedClass = null;
	
	public MysqlQuery(Class<T> clazz, MysqlNative jdbc, Datastore ds) {
		this.clazz = clazz;
		this.ds = ((MysqlDatastore)ds);
		//this.cache = this.ds.getMapper().createEntityCache();
		this.jdbc = jdbc;
		mappedClass = this.ds.getMapper().getMappedClass(clazz);
		
		fieldString = "*";
		whereList = new ArrayList();
	}
	
	public MysqlQuery(Class<T> clazz, Datastore ds, MysqlNative jdbc, int offset, int limit) {
		this(clazz, jdbc, ds);
		this.offset = offset;
		this.limit = limit;
	}
	
	@Override
	public MysqlQuery<T> clone(){
		MysqlQuery<T> n = new MysqlQuery<T>(clazz, jdbc, ds);
		//n.cache = this.ds.getMapper().createEntityCache(); // fresh cache
		n.fields = fields == null ? null : Arrays.copyOf(fields, fields.length);
		n.includeFields = includeFields;
		n.limit = limit;
		n.offset = offset;
		n.validateName = validateName;
		n.validateType = validateType;
		n.returnNull = returnNull;
		return n;
	}

	public int getOffset() {
		return offset;
	}

	public int getLimit() {
		return limit;
	}
	
	public MysqlDatastore getDatastore() {
		return ds;
	}
	
	public boolean isValidatingNames() {
		return validateName;
	}
	
	public boolean isValidatingTypes() {
		return validateType;
	}
	
	public long countAll() throws DataAccessException {
		if (returnNull)
			return 0;
		return jdbc.queryForLong(getCountString(), getFilterObjects());
	}

	@SuppressWarnings("unchecked")
	public List<T> asList() throws DataAccessException {
		if (returnNull)
			return new ArrayList();
		String sql = getQueryString();
		if (log.isInfoEnabled()) log.info("Query for list." + sql);
		List<T> list = jdbc.queryForList(sql, getFilterObjects(), clazz);
		if (list == null)
			return new ArrayList();
		return list;
	}
	
	public String getFilterString() {
		return whereString;
	}

	private String getQueryString() {
		String sql = "select " + fieldString + " from " + mappedClass.getCollectionName();
		if (whereString != null && !whereString.equals("")) {
			sql += " where " + whereString;
		}
		if (orderString != null && !orderString.equals("")) {
			sql += " order by " + orderString;
		}
		if (limit > 0) {
			sql += " LIMIT " + offset + "," + limit;
		}
		return sql;
	}
	
	private String getCountString() {
		String sql = "select count(1) from " + mappedClass.getCollectionName();
		if (whereString != null && !whereString.equals("")) {
			sql += " where " + whereString;
		}
		if (orderString != null && !orderString.equals("")) {
			sql += " order by " + orderString;
		}
		if (limit > 0) {
			sql += " LIMIT " + offset + "," + limit;
		}
		return sql;
	}

	public Object[] getFilterObjects() {
		if (whereList == null || whereList.size() < 1)
			return null;
		
		int count = whereList.size();
		Object[] objs = new Object[count];
		for (int i=0; i<count; i++) {
			objs[i] = whereList.get(i);
		}
		return objs;
	}

	public List<Key<T>> asKeyList() throws DataAccessException {
		if (returnNull)
			return new ArrayList();
		fieldString = "id";
		List<T> list = asList();
		if (list == null || list.size() < 1) {
			return new ArrayList();
		}
		List<Key<T>> keys = new ArrayList();
		for (T t : list) {
			Key<T> k = covertToKey(t);
			keys.add(k);
		}
		return keys;
	}
	
	private Key<T> covertToKey(T t) {
		try {
			Method m = t.getClass().getMethod("getId", null);
			return new Key<T>((Class<? extends T>) t.getClass(), m.invoke(t, null));
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	public Query<T> filter(String condition, Object value) {
		String[] ss = condition.trim().split(" ");
		if (ss == null || ss.length < 2)
			return this;
		
		String opt = getOpt(ss);
		String val = "?";
		boolean addToList = true;
		if (opt.equals("!=")) {
			opt = "<>";
		}
		else if (opt.equals("in")) {
			opt = "in";
			val = getInList(value);
			if (val == null) {
				returnNull = true;
			}
			addToList = false;
		}
		else if (opt.equals("nin")) {
			opt = "not in";
			val = getInList(value);
			if (val == null)
				return this;
			addToList = false;
		}
		else if (opt.equals("exists")) {
			opt = "is";
			if (Boolean.valueOf(value.toString()) == true) {
				val = "not null";
			}
			else {
				val = "null";
			}
			addToList = false;
		}
		else if (opt.equals("like")) {
			opt = "like";
			val = value.toString();
			addToList = false;
		}
		else if (opt.equals("neq")) {
			opt = "<>";
			val = value.toString();
			addToList = false;
		}
		else if (opt.equals("eq")) {
			opt = "=";
			val = value.toString();
			addToList = false;
		}

		if (whereString != null && !whereString.equals("")) {
			whereString += " and ";
		}
		
		String field = ss[0];
		if (field.trim().equals("_id")) {
			field = "id";
		}
		whereString += "`" + field + "` " + opt + " " + val;
		if (addToList) {
			whereList.add(value);
		}
		return this;
	}

	private String getOpt(String[] ss) {
		for (int i=ss.length - 1; i>0; i--) {
			if (ss[i] != null && !ss[i].trim().equals(""))
				return ss[i];
		}
		
		throw new IllegalArgumentException();
	}

	private String getInList(Object value) {
		if (value == null)
			return null;
		String val = "(";
		Class type = value.getClass();
		if (value instanceof List) {
			List objs = (List)value;
			if (objs.size() < 1)
				return null;
			
			boolean hasque = false;
			if (objs.get(0) instanceof String) {
				hasque = true;
			}
			
			for (Object o : objs) {
				val += hasque ? "'" + o.toString() + "'" : o.toString();
				val += ",";
			}
			
			val = val.substring(0, val.length() - 1) + ")";
		}
		else if (value instanceof Set) {
			Set objs = (Set)value;
			if (objs.size() < 1)
				return null;
			
			boolean hasque = false;
			if (objs.iterator().next() instanceof String) {
				hasque = true;
			}
			
			for (Object o : objs) {
				val += hasque ? "'" + o.toString() + "'" : o.toString();
				val += ",";
			}
			
			val = val.substring(0, val.length() - 1) + ")";
		}
		else {
			int count = Array.getLength(value);
			if (count < 1)
				return null;
			
			boolean hasque = false;
			if (Array.get(value, 0) instanceof String) {
				hasque = true;
			}
			
			for (int i=0; i<count; i++) {
				val += hasque ? "'" + Array.get(value, i) + "'" : Array.get(value, i);
				val += ",";
			}
			
			val = val.substring(0, val.length() - 1) + ")";
		}
		return val;
	}
	
	public Query<T> where(String sql) {
		whereString += " " + sql;
		return this;
	}
	
	public Query<T> where(String sql, Object...value) {
		whereString += " " + sql;
		if (value != null) {
			for (Object obj : value)
				whereList.add(value);
		}
		return this;
	}

	public Query<T> enableValidation(){ validateName = validateType = true; return this; }

	public Query<T> disableValidation(){ validateName = validateType = false; return this; }
	
	public T get() throws DataAccessException {
		String sql = getQueryString();
		if (log.isInfoEnabled()) log.info("Query for object." + sql);
		return (T)jdbc.queryForObject(sql, getFilterObjects(), clazz);
	}

	public Query<T> limit(int value) {
		this.limit = value;
		return this;
	}
	
	public Query<T> skip(int value) {
		this.offset = value;
		return this;
	}

	public Query<T> offset(int value) {
		this.offset = value;
		return this;
	}
	

	public Query<T> order(String condition) {
		String[] ss = condition.split(",");
		if (ss == null || ss.length < 1)
			return this;
		
		if (orderString != null && orderString.length() > 0) {
			orderString += ",";
		}
		for (String s : ss) {
			String str = s.startsWith("-") ? s.substring(1) : s;
			if (str.trim().equals("_id")) {
				str = "id";
			}
			this.orderString += s.startsWith("-") ? "`" + str + "` desc," : "`" + str + "`,";
		}
		this.orderString = this.orderString.substring(0, this.orderString.length() - 1);
		return this;
	}
	
	public Class<T> getEntityClass() {
		return this.clazz;
	}
	
	public String toString() {
		return getQueryString();
	}
	
	public Query<T> retrievedFields(boolean include, String...fields){
		Field[] list = clazz.getFields();
		int count = list.length;
		for (int i=count-1; i>=0; i--) {
			Field mf = list[i];
			if (isFound(mf, fields)) {
				if (!include) {
					list[i] = null;
				}
			}
			else {
				if (include) {
					list[i] = null;
				}
			}
		}
		if (list == null || list.length < 1)
			return this;
		
		fieldString = "";
		for (Field mf : list) {
			if (mf == null)
				continue;
			fieldString += mf.getName() + ",";
		}
		if (fieldString == null || fieldString.equals("")) {
			fieldString = "*";
		}
		else {
			fieldString = fieldString.substring(0, fieldString.length() - 1);
		}
		return this;
	}

	private boolean isFound(Field mf, String[] fs) {
		for (String n : fs) {
			if (n.trim().equals("_id") && mf.getName().toLowerCase().equals("id"))
				return true;
			
			if (mf.getName().toLowerCase().equals(n.toLowerCase()))
				return true;
		}
		return false;
	}

	public Iterator iterator() {
		try {
			return asList().iterator();
		} catch (DataAccessException e) {
			e.printStackTrace();
			return null;
		}
	}
}
